/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.sparql.expr;

import static org.apache.jena.datatypes.xsd.XSDDatatype.*;
import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_DIFFERENT;
import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_UNKNOWN;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Set;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.jena.atlas.lib.Cache;
import org.apache.jena.atlas.lib.CacheFactory;
import org.apache.jena.atlas.lib.DateTimeUtils;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.cdt.CompositeDatatypeList;
import org.apache.jena.cdt.CompositeDatatypeMap;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.datatypes.TypeMapper;
import org.apache.jena.ext.xerces.DatatypeFactoryInst;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.TextDirection;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.engine.ExecutionContext;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.expr.nodevalue.*;
import org.apache.jena.sparql.function.FunctionEnv;
import org.apache.jena.sparql.graph.NodeConst;
import org.apache.jena.sparql.graph.NodeTransform;
import org.apache.jena.sparql.serializer.SerializationContext;
import org.apache.jena.sparql.sse.SSE;
import org.apache.jena.sparql.util.FmtUtils;
import org.apache.jena.sparql.util.NodeFactoryExtra;
import org.apache.jena.sparql.util.XSDNumUtils;
import org.apache.jena.sys.JenaSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class NodeValue extends ExprNode
{
    static { JenaSystem.init(); }

    /*
     * Implementation notes:
     *
     * Delaying turning a value into a graph Node is
     * valuable because intermediates, like the result of 2+3, will not
     * be needed as nodes unless used for assignment.
     *
     * Operations:
     * See also NV* for NodeValue code.
     * See also NodeValueCmp for comparison operations.
     * See also XSDFuncOp for XQuery/XPath functions.
     * See also NodeFunctions for RDF Term related functions.
     *
     *
     * Note that SPARQL "=" is "known to be sameValueAs".
     * Similarly "!=" is known to be different.
     */

    /*
     * Effective boolean value rules.
     *    boolean: value of the boolean
     *    string: length(string) > 0 is true
     *    numeric: number != NaN && number != 0 is true
     * ref:  http://www.w3.org/TR/xquery/#dt-ebv
     */

    static Logger log = LoggerFactory.getLogger(NodeValue.class);

    public static boolean VerboseWarnings = true;
    public static boolean VerboseExceptions = false;

    // Before constants
    public static DatatypeFactory xmlDatatypeFactory =  DatatypeFactoryInst.newDatatypeFactory();
    private static int NODEVALUE_CACHE_SIZE = 10_000;
    private static Set<RDFDatatype> noCache = Set.of(CompositeDatatypeList.datatype(), CompositeDatatypeMap.datatype());
    private static Cache<Node, NodeValue> nodeValueCache = CacheFactory.createCache(NODEVALUE_CACHE_SIZE);

    public static final NodeValue TRUE   = NodeValue.makeNode("true", XSDboolean);
    public static final NodeValue FALSE  = NodeValue.makeNode("false", XSDboolean);

    public static final NodeValue nvZERO = NodeValue.makeNode(NodeConst.nodeZero);
    public static final NodeValue nvNegZERO = NodeValue.makeNode("-0.0e0", XSDdouble);
    public static final NodeValue nvONE  = NodeValue.makeNode(NodeConst.nodeOne);
    public static final NodeValue nvTEN  = NodeValue.makeNode(NodeConst.nodeTen);

    public static final NodeValue nvDecimalZERO = NodeValue.makeNode("0.0", XSDdecimal);
    public static final NodeValue nvDecimalONE  = NodeValue.makeNode("1.0", XSDdecimal);

    public static final NodeValue nvNaN     = NodeValue.makeNode("NaN", XSDdouble);
    public static final NodeValue nvINF     = NodeValue.makeNode("INF", XSDdouble);
    public static final NodeValue nvNegINF  = NodeValue.makeNode("-INF",XSDdouble);

    public static final NodeValue nvEmptyString  = NodeValue.makeString("");

    public static final String xsdNamespace = XSD+"#";

    private Node node = null;     // Null used when a value has not been turned into a Node.

    protected NodeValue() { super(); }
    protected NodeValue(Node n) { super(); node = n; }

    @Override
    public Set<Var> getVarsMentioned() {
        return Set.of();
    }

    /**
     * Convenience operation,primarily for tests.
     * Parse a string (using {@link SSE}) to produce a NodeValue - common namespaces like xsd: are built-in
     */
    public static NodeValue parse(String string)
    { return makeNode(NodeFactoryExtra.parseNode(string)); }

    // ----------------------------------------------------------------
    // ---- Construct NodeValue without a graph node (calculated later if needed)

    public static NodeValue makeInteger(long i)
    { return new NodeValueInteger(BigInteger.valueOf(i)); }

    public static NodeValue makeInteger(BigInteger i)
    { return new NodeValueInteger(i); }

    public static NodeValue makeInteger(String lexicalForm)
    { return new NodeValueInteger(new BigInteger(lexicalForm)); }

    public static NodeValue makeFloat(float f)
    { return new NodeValueFloat(f); }

    public static NodeValue makeDouble(double d)
    { return new NodeValueDouble(d); }

    public static NodeValue makeString(String s)
    { return new NodeValueString(s); }

    public static NodeValue makeSortKey(String s, String collation)
    { return new NodeValueSortKey(s, collation); }

    public static NodeValue makeLangString(String s, String lang)
    { return new NodeValueLang(s, lang); }

    public static NodeValue makeDirLangString(String s, String lang, String langDir)
    { return new NodeValueLangDir(s, lang, langDir); }

    public static NodeValue makeDirLangString(String s, String lang, TextDirection textDirection)
    { return new NodeValueLangDir(s, lang, textDirection); }

    public static NodeValue makeDecimal(BigDecimal d)
    { return new NodeValueDecimal(d); }

    public static NodeValue makeDecimal(long i)
    { return new NodeValueDecimal(BigDecimal.valueOf(i)); }

    public static NodeValue makeDecimal(double d)
    { return new NodeValueDecimal(BigDecimal.valueOf(d)); }

    public static NodeValue makeDecimal(String lexicalForm)
    { return NodeValue.makeNode(lexicalForm, XSDdecimal); }

    public static NodeValue makeDateTime(String lexicalForm)
    { return NodeValue.makeNode(lexicalForm, XSDdateTime); }

    public static NodeValue makeDate(String lexicalForm)
    { return NodeValue.makeNode(lexicalForm, XSDdate); }

    /** @deprecated Use a XMLGregorianCalendar */
    @Deprecated(forRemoval=true)
    public static NodeValue makeDateTime(Calendar cal) {
        String lex = DateTimeUtils.calendarToXSDDateTimeString(cal);
        return NodeValue.makeNode(lex, XSDdateTime);
    }

    public static NodeValue makeDateTime(XMLGregorianCalendar cal) {
        String lex = cal.toXMLFormat();
        Node node = NodeFactory.createLiteralDT(lex, XSDdateTime);
        return new NodeValueDateTime(cal, node);
    }

    /** @deprecated Use a XMLGregorianCalendar */
    @Deprecated(forRemoval=true)
    public static NodeValue makeDate(Calendar cal) {
        String lex = DateTimeUtils.calendarToXSDDateString(cal);
        return NodeValue.makeNode(lex, XSDdate);
    }

    public static NodeValue makeDate(XMLGregorianCalendar cal) {
        String lex = cal.toXMLFormat();
        Node node = NodeFactory.createLiteralDT(lex, XSDdate);
        return new NodeValueDateTime(cal, node);
    }

    public static NodeValue makeDuration(String lexicalForm)
    { return NodeValue.makeNode(lexicalForm, XSDduration); }

    public static NodeValue makeDuration(Duration duration)
    { return new NodeValueDuration(duration); }

    public static NodeValue makeBoolean(boolean b)
    { return b ? NodeValue.TRUE : NodeValue.FALSE; }

    public static NodeValue booleanReturn(boolean b)
    { return b ? NodeValue.TRUE : NodeValue.FALSE; }

    // ----------------------------------------------------------------
    // ---- Construct NodeValue from graph nodes

    public static NodeValue makeNode(Node node) {
        return nodeToNodeValue(node);
    }

    public static NodeValue makeNode(String lexicalForm, RDFDatatype dtype) {
        Node n = NodeFactory.createLiteralDT(lexicalForm, dtype);
        return makeNode(n);
    }

    // Convenience - knows that lang tags aren't allowed with datatypes.
    public static NodeValue makeNode(String lexicalForm, String langTag, Node datatype) {
        String uri = (datatype == null) ? null : datatype.getURI();
        return makeNode(lexicalForm, langTag, uri);
    }

    public static NodeValue makeNode(String lexicalForm, String langTag, String datatype) {
        if ( datatype != null && datatype.equals("") )
            datatype = null;

        if ( langTag != null && datatype != null )
            // raise??
            Log.warn(NodeValue.class, "Both lang tag and datatype defined (lexcial form '" + lexicalForm + "')");

        Node n = null;
        if ( langTag != null )
            n = NodeFactory.createLiteralLang(lexicalForm, langTag);
        else if ( datatype != null ) {
            RDFDatatype dType = TypeMapper.getInstance().getSafeTypeByName(datatype);
            n = NodeFactory.createLiteralDT(lexicalForm, dType);
        } else
            n = NodeFactory.createLiteralString(lexicalForm);

        return NodeValue.makeNode(n);
    }

    // ----------------------------------------------------------------
    // ---- Construct NodeValue with graph node and value.

    public static NodeValue makeNodeBoolean(boolean b)
    { return b ? NodeValue.TRUE : NodeValue.FALSE; }

    public static NodeValue makeNodeBoolean(String lexicalForm)
    { return makeNode(lexicalForm, XSDboolean); }

    public static NodeValue makeNodeInteger(long v)
    { return makeNode(Long.toString(v), XSDinteger); }

    public static NodeValue makeNodeInteger(String lexicalForm)
    { return makeNode(lexicalForm, XSDinteger); }

    public static NodeValue makeNodeFloat(float f)
    { return makeNode(XSDNumUtils.stringForm(f), XSDfloat); }

    public static NodeValue makeNodeFloat(String lexicalForm)
    { return makeNode(lexicalForm, XSDfloat); }

    public static NodeValue makeNodeDouble(double v)
    { return makeNode(XSDNumUtils.stringForm(v), XSDdouble); }

    public static NodeValue makeNodeDouble(String lexicalForm)
    { return makeNode(lexicalForm, XSDdouble); }

    public static NodeValue makeNodeDecimal(BigDecimal decimal) {
        String lex = XSDNumUtils.stringFormatARQ(decimal);
        return makeNode(lex, XSDdecimal);
    }

    public static NodeValue makeNodeDecimal(String lexicalForm)
    { return makeNode(lexicalForm, XSDdecimal); }

    public static NodeValue makeNodeString(String string)
    { return makeNode(string, XSDstring); }

    public static NodeValue makeNodeDateTime(String lexicalForm)
    { return makeNode(lexicalForm, XSDdateTime); }

    public static NodeValue makeNodeDate(String lexicalForm)
    { return makeNode(lexicalForm, XSDdate); }

    public static NodeValue makeNodeDuration(Duration duration, Node node)
    { return new NodeValueDuration(duration, node); }

    // ----------------------------------------------------------------
    // ---- Expr interface

    @Override
    public NodeValue eval(Binding binding, FunctionEnv env) {
        return this;
    }

    // NodeValues are immutable so no need to duplicate.
    @Override
    public Expr copySubstitute(Binding binding) {
        return this;
    }

    @Override
    public Expr applyNodeTransform(NodeTransform transform) {
        Node n = asNode();
        n = transform.apply(n);
        return ExprLib.nodeToExpr(n);
    }

    public Node evalNode(Binding binding, ExecutionContext execCxt) {
        return asNode();
    }

    @Override
    public boolean isConstant()     { return true; }

    @Override
    public NodeValue getConstant()  { return this; }

    public boolean isIRI() {
        forceToNode();
        return node.isURI();
    }

    public boolean isBlank() {
        forceToNode();
        return node.isBlank();
    }

    public boolean isTripleTerm() {
        forceToNode();
        return node.isTripleTerm();
    }

    public ValueSpace getValueSpace() {
        return classifyValueSpace(this);
    }

    public static ValueSpace classifyValueOp(NodeValue nv1, NodeValue nv2) {
        ValueSpace c1 = classifyValueSpace(nv1);
        ValueSpace c2 = classifyValueSpace(nv2);
        if ( c1 == c2 ) return c1;
        if ( c1 == VSPACE_UNKNOWN || c2 == VSPACE_UNKNOWN )
            return VSPACE_UNKNOWN;

        // Known values spaces but incompatible
        return VSPACE_DIFFERENT;
    }

    /*package*/ static ValueSpace classifyValueSpace(NodeValue nv) {
        return ValueSpace.valueSpace(nv);
    }

    // ----------------------------------------------------------------
    // ---- sameValueAs

    // Disjoint value spaces : dateTime and dates are not comparable
    // Every langtag implies another value space as well.

    /**
     * Return true if the two NodeValues are known to be the same value return false
     * if known to be different values, throw ExprEvalException otherwise
     */
    public static boolean sameValueAs(NodeValue nv1, NodeValue nv2) {
        return NVCompare.sameValueAs(nv1, nv2);
    }

    /**
     * Return true if the two Nodes are known to be different, return false if the
     * two Nodes are known to be the same, else throw ExprEvalException
     */
    public static boolean notSameValueAs(Node n1, Node n2) {
        return notSameValueAs(NodeValue.makeNode(n1), NodeValue.makeNode(n2));
    }

    /**
     * Return true if the two NodeValues are known to be different, return false if
     * the two NodeValues are known to be the same, else throw ExprEvalException
     */
    public static boolean notSameValueAs(NodeValue nv1, NodeValue nv2) {
        return NVCompare.notSameValueAs(nv1, nv2);
    }

    // ----------------------------------------------------------------
    // compare

    /** Compare by value (and only value) if possible.
     *  Supports &lt;, &lt;=, &gt;, &gt;= but not = nor != (which are sameValueAs and notSameValueAs)
     * @param nv1
     * @param nv2
     * @return Expr.CMP_LESS(-1), Expr.CMP_EQUAL(0) or Expr.CMP_GREATER(+1)
     * @throws ExprNotComparableException for Expr.CMP_INDETERMINATE(+2)
     */
    public static int compare(NodeValue nv1, NodeValue nv2) {
        //return NodeValueCompare.compare(nv1, nv2);
        int x = NVCompare.compareByValue(nv1, nv2);
        if ( x == Expr.CMP_INDETERMINATE || x == Expr.CMP_UNEQUAL )
            throw new ExprNotComparableException(null);
        return x;
    }
    /**
     * Compare by value if possible else compare by kind/type/lexical form
     * Only use when you want an ordering regardless of form of NodeValue,
     * for example in ORDER BY
     *
     * @param nv1
     * @param nv2
     * @return negative, 0, or positive for less than, equal, greater than.
     */
    public static int compareAlways(NodeValue nv1, NodeValue nv2) {
        //return NodeValueCompare.compareAlways(nv1, nv2);
        return NVCompare.compareAlways(nv1, nv2);
    }

    // ----------------------------------------------------------------
    // ---- Node operations

    public static Node toNode(NodeValue nv)
    {
        if ( nv == null )
            return null;
        return nv.asNode();
    }

    public final Node asNode()
    {
        if ( node == null )
            node = makeNode();
        return node;
    }
    protected abstract Node makeNode();

    /** getNode - return the node form - may be null (use .asNode() to force to a node) */
    public Node getNode() { return node; }

    public String getDatatypeURI() { return asNode().getLiteralDatatypeURI(); }

    public boolean hasNode() { return node != null; }

    // ----------------------------------------------------------------
    // ---- Subclass operations
    // "isX" means "can it be used where X is expected", according to the XPath/XQuery Functions and Operator rules.
    // e.g. NodeValueFloat.isDouble is true.

    public boolean isLiteral()      { return getNode() == null || getNode().isLiteral(); }

    public boolean isBoolean()      { return false; }
    public boolean isString()       { return false; }
    public boolean isLangString()   { return false; }
    public boolean isSortKey()      { return false; }

    public boolean isNumber()       { return false; }
    public boolean isInteger()      { return false; }
    public boolean isDecimal()      { return false; }
    public boolean isFloat()        { return false; }
    public boolean isDouble()       { return false; }

    public boolean hasDateTime()    { return isDateTime() || isDate() || isTime() || isGYear() || isGYearMonth() || isGMonth() || isGMonthDay() || isGDay(); }
    public boolean isDateTime()     { return false; }
    public boolean isDate()         { return false; }
    public boolean isTime()         { return false; }

    public boolean isDuration()     { return false; }
    public boolean isYearMonthDuration() { return isDuration() && NVOps.isYearMonthDuration(getDuration()); }
    public boolean isDayTimeDuration()   { return isDuration() && NVOps.isDayTimeDuration(getDuration()); }

    public boolean isGYear()        { return false; }
    public boolean isGYearMonth()   { return false; }
    public boolean isGMonth()       { return false; }
    public boolean isGMonthDay()    { return false; }
    public boolean isGDay()         { return false; }

    public boolean     getBoolean()     { raise(new ExprEvalTypeException("Not a boolean: "+this)); return false; }
    public String      getString()      { raise(new ExprEvalTypeException("Not a string: "+this)); return null; }
    public String      getLang()        { raise(new ExprEvalTypeException("Not a lang string: "+this)); return null; }
    public String      getLangDir()     { raise(new ExprEvalTypeException("Not a langdir string: "+this)); return null; }
    public NodeValueSortKey getSortKey()        { raise(new ExprEvalTypeException("Not a sort key: "+this)); return null; }

    public BigInteger  getInteger()     { raise(new ExprEvalTypeException("Not an integer: "+this)); return null; }
    public BigDecimal  getDecimal()     { raise(new ExprEvalTypeException("Not a decimal: "+this)); return null; }
    public float       getFloat()       { raise(new ExprEvalTypeException("Not a float: "+this)); return Float.NaN; }
    public double      getDouble()      { raise(new ExprEvalTypeException("Not a double: "+this)); return Double.NaN; }
    // Value representation for all date and time values.
    public XMLGregorianCalendar getDateTime()    { raise(new ExprEvalTypeException("No DateTime value: "+this)); return null; }
    public Duration    getDuration() { raise(new ExprEvalTypeException("Not a duration: "+this)); return null; }

    // ----------------------------------------------------------------

    private static NodeValue nodeToNodeValue(Node node) {
        if ( node.isLiteral() ) {
            // XXX Could have a set of all support datatypes in NVDatatypes
            RDFDatatype dt = node.getLiteralDatatype();
            if ( noCache.contains(dt) ) {
                // Composite datatypes (CDT) are not cached.
                return NodeValue.nodeToNodeValueMaker(node);
            }
        }
        if ( node.isExt() ) {
            // Don't judge custom extensions.
            return new NodeValueNode(node);
        }
        NodeValue nv = nodeValueCache.get(node, NodeValue::nodeToNodeValueMaker);
        //NodeValue nv = NodeValue.nodeToNodeValueMaker(node);
        return nv;
    }

    /**
     * Always returns a NodeValue of some kind.
     *
     * If the literal is ill-formed for the datatype,
     * then a {@link NodeValueNode} is returned
     */
    private static NodeValue nodeToNodeValueMaker(Node node) {
        if ( ! node.isConcrete() ) {
            String msg;
            if ( node.isVariable() )
                throw new ExprException("Variable passed to NodeValue.nodeToNodeValue: "+node);
            if ( node.isTripleTerm() )
                throw new ExprException("Triple term with a variable passed to NodeValue.nodeToNodeValue: "+node);
            // Should not happen.
            throw new ExprException("Node is not a constant");
        }

        // Includes creating NodeValueNode for ill-formed literals.
        NodeValue nv = NVFactory.create(node);
        return nv;
    }

    // ----------------------------------------------------------------

    /** Common point for exceptions during evaluation. */
    public static void raise(ExprException ex) {
        throw ex;
    }

    @Override
    public void visit(ExprVisitor visitor) { visitor.visit(this); }

    private void forceToNode() {
        if ( node == null )
            node = asNode();
        if ( node == null )
            raise(new ExprEvalException("Not a node: " + this));
    }

    // ---- Formatting (suitable for SPARQL syntax).
    // Usually done by being a Node and formatting that.
    // In desperation, will try toString() (no quoting)

    public final String asUnquotedString()
    { return asString(); }

    public final String asQuotedString()
    { return asQuotedString(new SerializationContext()); }

    public final String asQuotedString(SerializationContext context) {
        // If possible, make a node and use that as the formatted output.
        if ( node == null )
            node = asNode();
        if ( node != null )
            return FmtUtils.stringForNode(node, context);
        return toString();
    }

    // Convert to a string - usually overridden.
    public String asString() {
        forceToNode();
        return NodeFunctions.str(node);
    }

    @Override
    public int hashCode() {
        return asNode().hashCode();
    }

    @Override
    public boolean equals(Expr other, boolean bySyntax) {
        // Java equals, not "same value" or "same term"
        if ( other == null ) return false;
        if ( this == other ) return true;
        // This is the equality condition Jena uses - lang tags are different by case.
        if ( ! ( other instanceof NodeValue nv) )
            return false;
        return asNode().equals(nv.asNode());
    }

    public abstract void visit(NodeValueVisitor visitor);

    public Expr apply(ExprTransform transform)  { return transform.transform(this); }

    @Override
    public String toString() {
        return asQuotedString();
    }
}
